Lazy Loading
Lazy Loading & Code Splitting in React: Dynamic Imports for Performance
1. Problem Statement
Case Study: EduStream Learning Platform
EduStream is a large online education platform:
-
It offers courses, quizzes, video lectures, forums, and a dashboard—all as separate features.
-
Most users only use a few features per session (e.g., watching a video or taking a quiz).
-
Loading the entire app upfront slows down the initial experience, especially on slow networks.
-
The team wants to load only the code needed for the current page or feature, and fetch the rest on demand.

The challenge:
How do you split your React app into smaller bundles and load them only when needed—improving initial load times, reducing bandwidth, and keeping the user experience smooth?
2. Learning Objectives
By the end of this tutorial, you will:
-
Understand what code splitting and lazy loading are, and why they matter for React apps.
-
Use dynamic
import()
andReact.lazy()
to load components only when needed. -
Implement
React.Suspense
to handle loading states for lazy-loaded components. -
Apply both route-based and component-based code splitting for maximum performance.
-
Understand best practices, pitfalls, and how to handle errors and loading states.
3. Concept Introduction with Analogy
Analogy: The Modular Classroom
-
Imagine EduStream as a school with many classrooms (features).
-
Instead of opening all classrooms at once (loading all code), you unlock each classroom only when a student needs it (lazy loading).
-
The janitor (React) keeps the main hall (core app) open, but unlocks classrooms (feature bundles) on demand, saving energy and time.
4. Technical Deep Dive
A. What is Code Splitting & Lazy Loading?
-
Code splitting breaks your app into smaller chunks (bundles) so users only download what they need.
-
Lazy loading means loading code (components, modules) only when it’s actually required, not upfront.
-
Dynamic imports (
import()
) andReact.lazy()
are the main tools for this in React.
Why?
-
Faster initial load: Only essential code is loaded first.
-
Better user experience: Users interact with the app sooner.
-
Efficient resource use: Less bandwidth, less memory, especially for large apps and slow networks.
B. Dynamic Imports: The Foundation
-
Use
import()
to load modules asynchronously. -
Returns a promise that resolves to the module.
-
Can be used conditionally, in event handlers, or anywhere in your code—not just at the top.
// Only load the math module if needed
function handleCalculate() {
import('./math').then((math) => {
console.log(math.add(1, 2));
});
}
- This reduces the initial bundle size, as `math` is only loaded when `handleCalculate` is called .
C. React.lazy() and Suspense: Lazy Loading Components
1. React.lazy()
-
Wraps a dynamic import so you can use it as a component.
-
Loads the component only when it’s rendered for the first time.
import React, { Suspense } from 'react';
const VideoPlayer = React.lazy(() => import('./VideoPlayer'));
function App() {
return (
<div>
<h1>Welcome to EduStream</h1>
<Suspense fallback={<div>Loading video player...</div>}>
<VideoPlayer />
</Suspense>
</div>
);
}
- Suspense provides a fallback UI while the component is loading.
2. Route-Based Code Splitting
-
Ideal for large, distinct sections (e.g.,
/courses
,/dashboard
,/forum
). -
Each route loads its own bundle.
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { Suspense, lazy } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
const Courses = lazy(() => import('./Courses'));
const Forum = lazy(() => import('./Forum'));
function App() {
return (
<Router>
<Suspense fallback={<div>Loading page...</div>}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/courses" element={<Courses />} />
<Route path="/forum" element={<Forum />} />
</Routes>
</Suspense>
</Router>
);
}
- Only the code for the current route is loaded, reducing the initial bundle size.
3. Component-Based Code Splitting
-
Use for large, rarely-used, or non-essential components (e.g., a chart, modal, or admin panel).
-
Load the component only when needed, even within a page.
const Chart = React.lazy(() => import('./Chart'));
function AnalyticsPanel({ showChart }) {
return (
<div>
<h2>Analytics</h2>
{showChart && (
<Suspense fallback={<div>Loading chart...</div>}>
<Chart />
</Suspense>
)}
</div>
);
}
- This keeps the main bundle lean, loading heavy components only when used.
D. Best Practices and Pitfalls
Best Practices:
-
Start with route-based splitting, then optimize further with component-based splitting.
-
Always provide a good fallback UI with
Suspense
to avoid blank screens. -
Lazy load only non-critical components—keep headers, nav, and essential UI in the main bundle.
-
Use error boundaries to catch loading errors and show user-friendly messages.
-
Optimize chunk sizes with Webpack or Vite to avoid large lazy-loaded bundles.
Pitfalls:
-
Lazy loading too many small components can increase network requests and overhead.
-
Large lazy-loaded chunks can still cause delays—split wisely.
-
Forgetting
Suspense
fallback leads to blank screens while loading. -
Dynamic import paths must be static strings (not variables) for bundlers to split correctly.
5. Step-by-Step Data Modeling & Code Walkthrough
A. Lazy Loading a Feature Component
import React, { Suspense } from 'react';
// Lazy load the Quiz component
const Quiz = React.lazy(() => import('./Quiz'));
function CoursePage() {
const [showQuiz, setShowQuiz] = React.useState(false);
return (
<div>
<h2>Course Content</h2>
<button onClick={() => setShowQuiz(true)}>Take Quiz</button>
{showQuiz && (
<Suspense fallback={<div>Loading quiz...</div>}>
<Quiz />
</Suspense>
)}
</div>
);
}
- The Quiz code is only loaded when the user clicks "Take Quiz"
B. Route-Based Code Splitting Example
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { Suspense, lazy } from 'react';
const VideoLecture = lazy(() => import('./VideoLecture'));
const Forum = lazy(() => import('./Forum'));
function EduStreamApp() {
return (
<Router>
<Suspense fallback={<div>Loading page...</div>}>
<Routes>
<Route path="/lecture/:id" element={<VideoLecture />} />
<Route path="/forum" element={<Forum />} />
</Routes>
</Suspense>
</Router>
);
}
- Each page loads its own bundle, reducing initial load time.
C. Dynamic Import for Conditional Loading
function loadHelpWidget() {
import('./HelpWidget').then(({ default: HelpWidget }) => {
// Render or use HelpWidget as needed
});
}
- Useful for loading third-party libraries, modals, or admin tools only when needed.
6. Interactive Challenge / Mini-Project
Your Turn!
-
Create a
ProfileSettings
component that is only loaded when the user clicks a "Settings" button. -
Use
React.lazy()
andSuspense
to load the component with a loading spinner. -
Add a route
/admin
that lazy-loads anAdminPanel
component only when visited. -
Show how to handle loading errors with an error boundary.
7. Common Pitfalls & Best Practices
Common Pitfalls & Best Practices (Code Splitting & Lazy Loading)
Pitfall | Best Practice |
---|---|
Lazy loading too many small components | Focus on big, rarely-used, or route-level chunks |
No Suspense fallback | Always wrap lazy components in Suspense |
Large lazy-loaded chunks | Use Webpack/Vite to optimize chunk sizes |
Not handling loading errors | Use error boundaries for user-friendly errors |
Dynamic import paths as variables | Use static strings for import() paths |
9. Optional: Programmer’s Workflow Checklist
-
Use
React.lazy()
for dynamic imports of components. -
Wrap all lazy components in
Suspense
with a good fallback UI. -
Start with route-based code splitting, then optimize with component-based splitting.
-
Use error boundaries for robust error handling.
-
Test lazy loading on slow networks and devices.
-
Optimize chunk sizes and analyze bundles with Webpack/Vite tools.